package com.ssau.expert.core.impl;

import com.intelli.ray.core.Inject;
import com.intelli.ray.core.ManagedComponent;
import com.ssau.expert.common.*;
import com.ssau.expert.core.api.DatabaseCoreAPI;
import com.ssau.expert.core.api.ExpertCoreAPI;
import com.ssau.expert.core.model.Condition;
import com.ssau.expert.core.model.FuzzyRule;
import com.ssau.expert.core.model.Rule;
import com.ssau.expert.core.model.Variable;
import com.ssau.expert.core.util.ConditionHelper;
import com.ssau.expert.core.util.DataTypeHelper;
import com.ssau.expert.entity.Ship;
import com.ssau.expert.exception.SemanticException;
import com.ssau.expert.util.Names;
import com.ssau.expert.util.Pair;
import com.ssau.expert.util.QuestionHelper;
import com.sun.istack.internal.Nullable;

import java.util.*;

/**
 * Author: Sergey Saiyan
 * Created: 05.09.13 12:44
 */
@ManagedComponent(name = "SuggestHelper")
public class SuggestHelper {

    protected VariableTable table = VariableTable.get();

    protected Stack<Variable> variableStack = table.variableStack;

    @Inject
    protected DatabaseCoreAPI databaseCoreAPI;

    @Inject
    protected ExpertCoreAPI expertCoreAPI;

    protected Logger logger = Logger.get();

    private Question question = null;
    protected int lastRuleBarrier;

    protected Map<Variable, Collection<Rule>> checkedRulesForVariable = new HashMap<>();
    protected Map<Variable, Collection<Rule>> lowPriorityRuleMap = new HashMap<>();
    protected boolean lowPriorityFlag = false;

    protected Variable variableForPush = null;
    protected Variable targetVar = null;

    List<Pair<Rule, Integer>> activatedPhase1 = new ArrayList<>();
    List<Pair<Rule, Integer>> activatedPhase2 = new ArrayList<>();


    int phase = 0;

    protected void resetState() {
        question = null;
        checkedRulesForVariable.clear();
        variableForPush = null;
        logger.clearLog();
        table.resetTable();
        lowPriorityFlag = false;
        activatedPhase1.clear();
        activatedPhase2.clear();
        lowPriorityRuleMap.clear();
        lastRuleBarrier = 0;
        phase = 0;
    }

    public SuggestedResult doSuggestion() {
        resetState();
        /* Список всех правил */
        List<FuzzyRule> rules = new ArrayList<>(databaseCoreAPI.getFuzzyRules());
        Collections.reverse(rules);

        targetVar = table.getVariable(Names.RESULT);
        /* Помещаем в стек целевую переменную - результат */
        variableStack.push(targetVar);
        logger.info("Целевая переменная 'R' помещена в стек");

        boolean specialExitCondition = false;
        int iteration = 0;

        phase = 0;

        List<SuggestedResult> finalResults = new ArrayList<>();

        /* Пока не найдем решение либо что-нибудь не сломается */
        while (phase < 2) {
            iteration = 0;
            boolean exitLoopFlag = false;
            while (!targetVar.isInitialized() && !specialExitCondition && !exitLoopFlag) {
                iteration++;
                /* Если что-то отломалось */
                if (iteration == 100) {
                    specialExitCondition = true;
                    continue;
                }
                /* Правила, которые уже выполняли для данной переменной */
                Collection<Rule> checkedRules = checkedRulesForVariable.get(variableStack.peek());
                if (checkedRules == null) {
                    checkedRules = new ArrayList<>();
                    checkedRulesForVariable.put(variableStack.peek(), checkedRules);
                }

                /* Находим правило, имеющее := для целевой переменной */
                logger.info("Поиск правила, изменяющего переменную '%s'", variableStack.peek().getName());
                Rule targetRule = getTargetRule(rules, checkedRules);
                Collection<Rule> lowPriorityRules = lowPriorityRuleMap.get(variableStack.peek());
                if (lowPriorityRules == null) {
                    lowPriorityRules = new ArrayList<>();
                    lowPriorityRuleMap.put(variableStack.peek(), lowPriorityRules);
                }
                lowPriorityRules.add(targetRule);
                lowPriorityFlag = false;
                /* Если ни одного нет - система несовместна */
                if (targetRule == null) {
                    if (checkedRules.isEmpty()) {
                        logger.error("Не найдено ни одного правила, изменяющего переменную '%s'", variableStack.peek().getName());
                    } else {
                        if (activatedPhase1.isEmpty() && phase == 0 || activatedPhase2.isEmpty() && phase == 1) {
                            logger.warn("Проверено %s правил, в которых участвует целевая переменная, " +
                                    "но не найдено правило, изменяющее переменную '%s'", checkedRules.size(), variableStack.peek().getName());
                            finalResults.add(wrapResult(targetVar, 0));
                            exitLoopFlag = true;
                            continue;
                        } else {
                            if (activatedPhase1.size() == 1 && phase == 0 || activatedPhase2.size() == 1 && phase == 1) {
                                logger.info("Было активировано 1 правило '%s'. Выполняем часть THEN : '%s'",
                                        phase == 0
                                                ? activatedPhase1.get(0).getFirst().getRuleString()
                                                : activatedPhase2.get(0).getFirst().getRuleString(),
                                        phase == 0
                                                ? activatedPhase1.get(0).getFirst().getRuleOutput()
                                                : activatedPhase2.get(0).getFirst().getRuleOutput()
                                );

                                executeOutput(phase == 0
                                        ? activatedPhase1.get(0).getFirst().getRuleOutput()
                                        : activatedPhase2.get(0).getFirst().getRuleOutput());

                                finalResults.add(wrapResult(targetVar, phase == 0
                                        ? activatedPhase1.get(0).getSecond()
                                        : activatedPhase2.get(0).getSecond()));
                                exitLoopFlag = true;
                                continue;
                            } else {
                                logger.info("Было активировано %s правил. Фаза %s.", phase == 0
                                        ? activatedPhase1.size()
                                        : activatedPhase2.size(),
                                        phase);
                                //TODO
                                List<Pair<Rule, Integer>> activated = phase == 0
                                        ? activatedPhase1
                                        : activatedPhase2;
                                for (Pair<Rule, Integer> pair : activated) {
                                    Rule pairRule = pair.getFirst();
                                    executeOutput(pairRule.getRuleOutput());
                                    finalResults.add(wrapResult(targetVar, pair.getSecond()));
                                }
                                exitLoopFlag = true;
                                continue;
                            }
                        }
                    }
                    if (variableStack.size() > 1) {
                        logger.info("В стеке остались переменные. Попытка вернуться на предыдущий уровень цикла");
                        logger.info("Выталкивание переменной %s из стека", variableStack.peek().getName());
                        variableStack.pop();
                        lowPriorityFlag = true;
                        continue;
                    } else {
                        logger.info("Окончание консультации. Фаза %s", phase);
                        SuggestedResult res = new SuggestedResult();
                        res.setError("Не найдено ни одного правила, изменяющего значение\nцелевой переменной '"
                                + variableStack.peek().getName() + "'");
                        finalResults.add(res);
                        exitLoopFlag = true;
                        continue;
                    }
                }

                logger.info("Найдено правило %s: '%s'", targetRule.getId(), targetRule.getRuleString());
                logger.info("Начинается проверка конъюнктов в правиле");
                /* Проверяем истинно ли условие IF в правиле */
                CheckConjunctResult result = checkRuleConjunct(targetRule);
                /*
                *  Если проверка остановлена из за неиницализированной переменной - переводим эксперта в
                *  ожидание ввода от пользователя
                */
                while (result == CheckConjunctResult.QUESTION) {
                    logger.info("В правиле найдена неинициализированная переменная '%s'", question.getVariableName());
                    logger.info("Ожидание инициализации переменной пользователем");
                    expertCoreAPI.setState(ExpertSystem.State.WAITING_INPUT);
                    QuestionRequestListener listener = expertCoreAPI.getListener();
                    Answer answer = listener.onQuestionRequest(question, true);
                    if (answer == null) {
                        logger.warn("Пользователь прервал консультацию");
                        SuggestedResult sr = new SuggestedResult();
                        sr.setError("Пользователь прервал консультацию");
                        return sr;
                    }

                    logger.info("Ответ пользователя для инициализации переменной : '%s'", answer);
                    /* Пока пользователь вводит фигню, повторяем вопрос */
                    while (!checkAnswer(question, answer)) {
                        logger.warn("Некорректные данные, повторение вопроса");
                        answer = listener.onQuestionRequest(question, false);
                    }
                    expertCoreAPI.setState(ExpertSystem.State.INITIALIZED);
                    result = checkRuleConjunct(targetRule);
                }

                /* Если в ходе проверки условия была найдена неинициализированная переменная,
                *  которую нельзя спросить у пользователя, пушим ее в стек */
                if (result == CheckConjunctResult.NEW_VAR) {
                    logger.info("Помещение переменной '%s' в стек", variableForPush.getName());
                    variableStack.push(variableForPush);
                    continue;
                }

                /**
                 *  Если условие проверено и вопросов нет выполним THEN выражение
                 */
                if (result == CheckConjunctResult.TRUE) {
                    checkedRules.add(targetRule);

                    if (phase == 0) {
                        activatedPhase1.add(new Pair<>(targetRule, lastRuleBarrier));
                    } else {
                        activatedPhase2.add(new Pair<>(targetRule, lastRuleBarrier));
                    }

                    boolean continueSearch = false;
                    if (!Names.RESULT.equals(variableStack.peek().getName())) {
                        logger.info("Выталкиваем переменную '%s' из стека", variableStack.peek().getName());
                        /* Выкидываем верхушку стека */
                        variableStack.pop();
                    } else {
                        continueSearch = true;
                    }

                    Variable newVar = variableStack.isEmpty() ? null : variableStack.peek();

                    if (continueSearch) {
                        logger.info("Продолжаем поиск правил.");
                    } else {
                        /* Если стек не пустой - вертим алгоритм дальше*/
                        logger.info("Переменная на вершине стека : '%s'", newVar.getName());
                    }

                } else if (result == CheckConjunctResult.FALSE) {
                    checkedRules.add(targetRule);
                    logger.info("Условие '%s' ложно", targetRule.getConditionsString());
                } else if (result == CheckConjunctResult.BELOW_BARRIER) {
                    checkedRules.add(targetRule);
                    logger.info("Правило '%s' сработало, но коэффициент уверенности %s меньше порога", targetRule.getRuleString(), lastRuleBarrier);
                }
            }

            if (expertCoreAPI.getDistanceAnswer().getCf1() == 100 && expertCoreAPI.getStaffAnswer().getCf1() == 100) {
                phase = 2;
            } else {
                onPhaseIncrement();
            }
        }

        logger.info("Окончание консультации");
        return processResult(finalResults);
    }

    private SuggestedResult processResult(List<SuggestedResult> finalResults) {
        Map<Ship, Integer> map = new HashMap<>();

        for (SuggestedResult res : finalResults) {
            if (res.getSuggestedShip() != null && res.getCfShip() != 0) {
                if (map.containsKey(res.getSuggestedShip())) {
                    logger.info("Активация правил дало одинаковый результат для корабля %s. Вычисляем коэффициент уверенности по алгебре ECFCO", res.getSuggestedShip().getShipName().getLocalName());
                    map.put(res.getSuggestedShip(), OR(map.get(res.getSuggestedShip()), res.getCfShip()));
                } else {
                    map.put(res.getSuggestedShip(), res.getCfShip());
                }
            }

            if (res.getAlternateShips() != null && !res.getAlternateShips().isEmpty()) {
                List<Pair<Ship, Integer>> alts = res.getAlternateShips();
                for (Pair<Ship, Integer> alt : alts) {
                    if (map.containsKey(alt.getFirst())) {
                        logger.info("Активация правил дало одинаковый результат для корабля %s. Вычисляем коэффициент уверенности по алгебре ECFCO", alt.getFirst().getShipName().getLocalName());
                        map.put(alt.getFirst(), OR(map.get(alt.getFirst()), alt.getSecond()));
                    } else {
                        map.put(alt.getFirst(), alt.getSecond());
                    }
                }
            }
        }

        SuggestedResult res = new SuggestedResult();
        if (map.size() == 1) {
            res.setCfShip(map.entrySet().iterator().next().getValue());
            res.setSuggestedShip(map.entrySet().iterator().next().getKey());
        } else {
            List<Pair<Ship, Integer>> ships = new ArrayList<>();
            for (Ship ship : map.keySet()) {
                ships.add(new Pair<>(ship, map.get(ship)));
            }
            res.setAlternateShips(ships);
        }

        return res;
    }

    private void onPhaseIncrement() {
        phase++;

        if (phase != 1) {
            return;
        }

        logger.info("-----------------------------------");
        logger.info("Запускается вторая итерация проверки правил для вторых значений пользователя");
        logger.info("-----------------------------------");

        checkedRulesForVariable.clear();
        variableForPush = null;
        lowPriorityFlag = false;
        lowPriorityRuleMap.clear();
        lastRuleBarrier = 0;

        targetVar = table.getVariable(Names.RESULT);
        variableStack.clear();
        /* Помещаем в стек целевую переменную - результат */
        variableStack.push(targetVar);
        logger.info("Целевая переменная 'R' помещена в стек");
    }

    /* Создает SuggestedResult по строковому значению целевой переменной */
    private SuggestedResult wrapResult(Variable targetVar, int cf) {
        String value = targetVar.getValue1();
        if (value == null) {
            return null;
        }

        if (value.contains("[")) {
            value = value.substring(1, value.length() - 1);
        }


        if (value.isEmpty()) {
            return null;
        }

        List<Ship> ships = databaseCoreAPI.getShips();
        if (value.contains(",")) {
            String[] variants = value.split(",");

            SuggestedResult result = new SuggestedResult();
            List<Pair<Ship, Integer>> alternates = new ArrayList<>();
            result.setAlternateShips(alternates);

            for (String variant : variants) {
                if (variant.startsWith("\"")) {
                    variant = variant.substring(1, variant.length() - 1);
                }
                Ship foundShip = null;
                for (Ship ship : ships) {
                    if (ship.getShipName().getLocalName().equalsIgnoreCase(variant)) {
                        foundShip = ship;
                        break;
                    }
                }
                if (foundShip == null) {
                    throw new RuntimeException("Unknown ship : " + variant);
                }
                alternates.add(new Pair<>(foundShip, cf));
            }
            return result;
        } else {
            SuggestedResult result = new SuggestedResult();
            if (value.startsWith("\"")) {
                value = value.substring(1, value.length() - 1);
            }
            Ship foundShip = null;
            for (Ship ship : ships) {
                if (ship.getShipName().getLocalName().equalsIgnoreCase(value)) {
                    foundShip = ship;
                    break;
                }
            }
            if (foundShip == null) {
                throw new RuntimeException("Unknown ship : " + value);
            }
            result.setSuggestedShip(foundShip);
            result.setCfShip(cf);
            return result;
        }
    }

    //TODO мне лень писать здесь проверки, если правило кривое - все упадет.
    /* Выполняет вывод, описанный в правиле */
    private void executeOutput(String ruleOutput) {
        String[] args = ruleOutput.split(" ");
        String var = args[0].trim();
        String value = args[2].trim();
        Variable variable = table.getVariable(var.toUpperCase());
        variable.setValue1(value);
        variable.setInitialized(false);
    }

    /**
     * Проверяет, корректен ли ответ пользователя
     * Если ответ корректен, сразу же инициализирует переменную в вопросе.
     *
     * @param question Вопрос
     * @param answer   Ответ пользователя в виде строки
     * @return TRUE если все ОК
     */
    private boolean checkAnswer(Question question, Answer answer) {
        try {
            Variable variable = table.getVariable(question.getVariableName());
            Class clazz = variable.getClazz();
            Class vClass = DataTypeHelper.getClassByValue(answer.getValue1());
            if (!vClass.equals(clazz)) {
                return false;
            }
            if (String.class.equals(vClass)) {
                answer.setValue1(answer.getValue1().toUpperCase());
            }
            variable.setValue1(answer.getValue1());
            variable.setValue2(answer.getValue2());
            variable.setCf1(answer.getCf1());
            variable.setCf2(answer.getCf2());

            logger.info("Присваивание значения '%s' переменной '%s'", answer, variable.getName());

            variable.setAnswer(answer);
            return true;
        } catch (SemanticException ex) {
            return false;
        }
    }

    /**
     * Из списка правил возвращает подходящее, содержащее целевую переменную
     */
    @Nullable
    protected Rule getTargetRule(List<? extends Rule> rules, Collection<? extends Rule> excluded) {
        Rule targetRule = null;
        Collection<Rule> lowPriority = lowPriorityRuleMap.get(variableStack.peek());
        if (lowPriority == null) {
            lowPriority = new ArrayList<>();
            lowPriorityRuleMap.put(variableStack.peek(), lowPriority);
        }
        for (Rule rule : rules) {
            Variable outVar = rule.getOutputVariable();
            if (outVar.equals(variableStack.peek()) && !excluded.contains(rule)) {
                if (lowPriorityFlag && isRuleInCollection(rule, lowPriority)) {
                    continue;
                }
                targetRule = rule;
                break;
            }
        }
        return targetRule;
    }

    protected boolean isRuleInCollection(Rule rule, Collection<Rule> rules) {
        for (Rule rule_ : rules) {
            if (rule.getId().equals(rule_.getId())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Проверка, истино ли условие в правиле
     */
    protected CheckConjunctResult checkRuleConjunct(Rule rule) {
        boolean conjunctIsTrue = false;
        /* Проверяем каждое условие (связаны &&) */
        for (Condition condition : rule.getConditions()) {
            /* Переменная в условии */
            Variable conditionVar = condition.getVariable();
            /* Если не инициализирована - надо спросить у пользователя */
            if (!conditionVar.isInitialized()) {
                String questionString = QuestionHelper.getQuestionByVariableName(conditionVar.getName());
                /* Если можно спросить у пользователя */
                if (questionString != null) {
                    question = new Question(questionString);
                    question.setVariableName(conditionVar.getName());
                    return CheckConjunctResult.QUESTION;
                } else {
                    variableForPush = conditionVar;
                    return CheckConjunctResult.NEW_VAR;
                }
            } else {
                /* Если инициализирована - проверим истинность */
                Object value = (phase == 0 || conditionVar.getCf1() == 100)
                        ? conditionVar.getValue1()
                        : conditionVar.getValue2();
                if (value == null) {
                    throw new RuntimeException(String.format("Variable '%s' is marked as initialized, but value is null",
                            conditionVar.getName()));
                }
                boolean isTrue = ConditionHelper.isTrue(condition, phase);
                if (isTrue) {
                    conjunctIsTrue = true;
                } else {
                    conjunctIsTrue = false;
                    break;
                }
            }
        }

        if (!conjunctIsTrue) {
            return CheckConjunctResult.FALSE;
        }

        int ruleBarrier = checkFuzzy((FuzzyRule) rule);

        if (ruleBarrier >= expertCoreAPI.getUndefinedBarrier()) {
            lastRuleBarrier = ruleBarrier;
            logger.info("Правило сработало с коэффициентом уверенности %s", ruleBarrier);
            return CheckConjunctResult.TRUE;
        } else {
            lastRuleBarrier = ruleBarrier;
            return CheckConjunctResult.BELOW_BARRIER;
        }
    }

    // AM &&  - (min(CF1,CF2) + (CF1*CF2/100))/2       || - (max(CF1,CF2)
    // BP && - (CF1*CF2/100) * (2-max(CF1,CF2)/100)  || (CF1+CF2) - (Cf1*Cf2)/100
    protected int checkFuzzy(FuzzyRule fuzzyRule) {
        RuleDTO ruleDTO = fuzzyRule.getRuleDTO();

        int[] cfs = new int[4];

        int dCF = phase == 0 || table.getVariable(Names.DISTANCE).getCf1() == 100
                ? table.getVariable(Names.DISTANCE).getCf1()
                : table.getVariable(Names.DISTANCE).getCf2();

        int sCF = phase == 0 || table.getVariable(Names.STUFF).getCf1() == 100
                ? table.getVariable(Names.STUFF).getCf1()
                : table.getVariable(Names.STUFF).getCf2();

        int rALCF = (int) ruleDTO.values[RuleDTO.A_L_CF];
        int rARCF = (int) ruleDTO.values[RuleDTO.A_R_CF];
        int rSLCF = (int) ruleDTO.values[RuleDTO.S_L_CF];
        int rSRCF = (int) ruleDTO.values[RuleDTO.S_R_CF];

        logger.info("Подсчет CF для автономности (левая граница)");
        logger.info("CF1 = %s, CF2 = %s", rALCF, dCF);
        cfs[0] = AND(rALCF, dCF);

        logger.info("Подсчет CF для автономности (правая граница)");
        logger.info("CF1 = %s, CF2 = %s", rARCF, dCF);
        cfs[1] = AND(rARCF, dCF);

        logger.info("Подсчет CF для персонала (левая граница)");
        logger.info("CF1 = %s, CF2 = %s", rSLCF, sCF);
        cfs[2] = AND(rSLCF, sCF);

        logger.info("Подсчет CF для персонала (правая граница)");
        logger.info("CF1 = %s, CF2 = %s", rSRCF, sCF);
        cfs[3] = AND(rSRCF, sCF);

        logger.info("Подсчет итогового CF для условия");
        int cfCondition = AND(AND(cfs[0], cfs[1]), AND(cfs[2], cfs[3]));

        return cfCondition;
    }

    protected int AND(int cf1, int cf2) {
        String alg = expertCoreAPI.getAlgebra();
        if ("AM".equals(alg)) {
            int res = (Math.min(cf1, cf2) + cf1 * cf2 / 100) / 2;
            logger.info("[MIN(%s,%s) + %s * %s / 100] / 2 = %s", cf1, cf2, cf1, cf2, res);
            return res;
        } else {
            int res = (cf1 * cf2 / 100) * (int) (2 - Math.round((Math.max(cf1, cf2) * 1.0 / 100)));
            logger.info("(%s * %s / 100) * (2 - MAX(%s,%s)/100) = %s", cf1, cf2, cf1, cf2, res);
            return res;
        }
    }

    protected int OR(int cf1, int cf2) {
        String alg = expertCoreAPI.getAlgebra();
        if ("AM".equals(alg)) {
            int res = Math.max(cf1, cf2);
            logger.info("MAX(%s,%s) = %s", cf1, cf2, res);
            return res;
        } else {
            int res = cf1 + cf2 - cf1 * cf2 / 100;
            logger.info("%s + %s - %s * %s / 100 = %s", cf1, cf2, cf1, cf2, res);
            return res;
        }
    }

    protected enum CheckConjunctResult {
        QUESTION,
        TRUE,
        FALSE,
        NEW_VAR,
        BELOW_BARRIER
    }
}
